package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    //线程池
    private static final ExecutorService es = Executors.newFixedThreadPool(10);

    /**
     * 将任意Java对象序列为JSON，并保存到Redis中
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 将任意Java对象序列为JSON，并保存到Redis中，逻辑删除
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicDelete(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    //根据指定的key查询缓存，并反序列化为指定类型，使用缓存空值解决缓存穿透
    public <ID, R> R get(
            String keyPreFix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallback) {
        String cache = stringRedisTemplate.opsForValue().get(keyPreFix + id);
        if (StrUtil.isNotBlank(cache)) {
            //查到缓存了
            return JSONUtil.toBean(cache, type);
        }
        if (cache != null) {
            //查到的缓存是空值
            return null;
        }
        //走到这里的逻辑就代表缓存中没有数据

        //需要查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            //数据库里无信息
            stringRedisTemplate.opsForValue().set(keyPreFix + id, "", time, unit);
            return null;
        }
        //数据库有信息
        stringRedisTemplate.opsForValue().set(keyPreFix + id, JSONUtil.toJsonStr(r), time, unit);
        return r;
    }


    //根据指定的key查询缓存，并反序列化为指定类型，使用逻辑删除解决缓存击穿
    public <R, ID> R getWithLogicDelete(String keyPreFix, ID id, Class<R> type, Long expireSeconds,
                                        Function<ID, R> dbFallback) {
        String cache = stringRedisTemplate.opsForValue().get(keyPreFix + id);
        if (StrUtil.isBlank(cache)) {
            //缓存中没有数据,直接返回
            return null;
        }
        RedisData redisData = JSONUtil.toBean(cache, RedisData.class);
        R result = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            //缓存未过期
            return result;
            //return JSONUtil.toBean((JSONObject) data, type);
        }
        //缓存过期，尝试获取锁
        if (tryGetLock("lock:"+keyPreFix, id)) {
            //获取到了锁
            //获取到锁成功后，再次检查缓存中的数据是否过期
            String s = stringRedisTemplate.opsForValue().get(keyPreFix + id);
            RedisData bean = JSONUtil.toBean(s, RedisData.class);
            if (bean.getExpireTime().isAfter(LocalDateTime.now())) {
                //有线程已经更新了数据了,直接返回即可
                return JSONUtil.toBean((JSONObject) bean.getData(), type);
            }
        }
        es.submit(() -> {
            try {
                //缓存重建
                R r = dbFallback.apply(id);
                RedisData rd = new RedisData();
                rd.setData(r);
                rd.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
                stringRedisTemplate.opsForValue().set(keyPreFix + id, JSONUtil.toJsonStr(rd));
                //释放锁
            } finally {
                unlock("lock:"+keyPreFix, id);
            }
        });
        //获取锁失败，直接返回旧数据
        return result;
    }

    private <ID> boolean tryGetLock(String keyPreFix, ID id) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(keyPreFix + id, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private <ID> void unlock(String keyPreFix, ID id) {
        stringRedisTemplate.delete(keyPreFix + id);
    }

}
